package com.googlecode.autopp.driver;

import java.awt.Color;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.util.Date;

import com.googlecode.autopp.Driver;
import com.googlecode.autopp.ObjectContext;
import com.googlecode.autopp.conf.ExceptionStrategy;
import com.googlecode.autopp.conf.ExceptionStrategyImpl;
import com.googlecode.autopp.conf.NestingStrategy;
import com.googlecode.autopp.conf.NestingStrategyImpl;

/**
 * Base driver implementation for simple drivers. It also implements builder for
 * common Java types like String or numbers.
 */
public abstract class SimpleGenericDriver implements Driver, ValueBuilder {

    // default strategy for nesting objects
    protected NestingStrategy nestingStrategy = NestingStrategyImpl.NEW_OBJECT;

    // default strategy for exceptions thrown during creating objects
    protected ExceptionStrategy exceptionStrategy = ExceptionStrategyImpl.WARN;

    /**
     * Creates and populates object of given type.
     * 
     * @param <T>
     *            Type of created object.
     * @param objectContext
     *            Type of created object and other information wrapped into
     *            context object.
     * 
     * @return Populated object.
     */
    @Override
    public <T> T createAndPopulateObject(ObjectContext<T> contex) {
        if (contex.getClazz() == null) {
            throw new NullPointerException("Cannot populate object with the [null] type.");
        }
        // creating object with given strategy
        T object = nestingStrategy.create(contex);
        // sometimes we have null, so we need to chek this case
        if (object != null) {
            // checking setters from object
            for (Method method : contex.getClazz().getMethods()) {
                if (method.getName().matches("set[0-9A-Z][a-zA-Z0-9]*") && method.getParameterTypes().length == 1) {
                    try {
                        // populating with values
                        populateAttribute(object, method, method.getParameterTypes()[0], contex.getStep());
                    } catch (Exception e) {
                        exceptionStrategy.handleException(e);
                    }
                }
            }
        }
        return object;
    }

    /**
     * Populates attributes of given object.
     * 
     * @param <T>
     *            Attribute type.
     * @param object
     *            Parent object which holds reference to attribute.
     * @param method
     *            Setter to invoke.
     * @param argument
     *            Type to set in given setter,
     * @param step
     *            Nesting step.
     */
    @SuppressWarnings("unchecked")
    // TODO handle arrays and lists
    protected <T> void populateAttribute(T object, Method method, Class<?> argument, int step)
            throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
        // there's no switch for classes :(
        if (argument == String.class) {
            method.invoke(object, getString());
        } else if (argument == Date.class) {
            method.invoke(object, getDate());
        } else if (argument == Color.class) {
            method.invoke(object, getColor());
        } else if (argument == URI.class) {
            method.invoke(object, getURI());
        } else if (argument == URL.class) {
            method.invoke(object, getURL());
        } else if (argument == Integer.class || argument == int.class) {
            method.invoke(object, getInteger());
        } else if (argument == Long.class || argument == long.class) {
            method.invoke(object, getLong());
        } else if (argument == Double.class || argument == double.class) {
            method.invoke(object, getDouble());
        } else if (argument == Float.class || argument == float.class) {
            method.invoke(object, getFloat());
        } else if (argument == Boolean.class || argument == boolean.class) {
            method.invoke(object, getBoolean());
        } else if (argument == Byte.class || argument == byte.class) {
            method.invoke(object, getByte());
        } else if (argument.isEnum()) {
            method.invoke(object, getEnum(((Class<Enum<?>>)argument)));
        } else {
            // none of handled types, we use standard mechanism, i.e. recurrence
            method.invoke(object, createAndPopulateObject(createContext(argument, step + 1)));
        }
    }

    /**
     * Creates new object context with given type and step.
     * 
     * @param <Z>
     *            Type of created object.
     * @param clazz
     *            Type of created object.
     * @param step
     *            Object nesting number.
     * 
     * @return Newly created context.
     */
    private <Z> ObjectContext<Z> createContext(Class<Z> clazz, int step) {
        return new ObjectContext<Z>(clazz, step);
    }

    /**
     * Sets strategy with nesting objects.
     */
    @Override
    public void setNestingStrategy(NestingStrategy strategy) {
        this.nestingStrategy = strategy;
    }

    /**
     * Sets strategy for exceptions handling.
     */
    @Override
    public void setExceptionStrategy(ExceptionStrategy strategy) {
        this.exceptionStrategy = strategy;
    }

}
